package com.hapiware.asm.timemachine; import java.lang.instrument.Instrumentation; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.Calendar; import java.util.Date; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * {@code TimeMachineAgentDelegate} can be used to shift system time <i>without touching the system * clock</i>. The trick is to catch all the system time calls and manipulate their byte code * directly to achieve the desired result. Time shift is done only for some specific classes which * can be defined in the configuration file. * <p> * * The shift of system time can be relative or absolute. Relative system time shift means that * some offset is added to or subtracted from the original system time value. Absolute system * time shift means that original system time is always replaced by some absolute value. * <p> * * {@code TimeMachineAgentDelegate} is specified in the agent configuration XML file using * {@code /agent/delegate} element. For example: * <xmp> * <?xml version="1.0" encoding="UTF-8" ?> * <agent> * <delegate>com.hapiware.asm.timemachine.TimeMachineAgentDelegate</delegate> * ... * </agent> * </xmp> * * * * <h3>Requirements</h3> * * {@code TimeMachineAgentDelegate} requires {@code com.hapiware.agent.Agent}. For more * information see {@code com.hapiware.agent.Agent}. Also an ASM 3.0 or later is needed * (see <a href="http://asm.ow2.org/" target="_blank">http://asm.ow2.org/</a>). * * * <h3>Time shift configuration</h3> * Time shift is configured using {@code /agent/configuration} element with a single valid * {@code String} object which presents time shift. * * The {@code /agent/configuration} element has the following format:<br> * <blockquote> * {@code <time>[+|-]y-M-d@H:m:s</time>} * </blockquote> * * where: * <ul> * <li> * {@code +} or {@code -} <b>Optional</b>. Indicates if the configured time is * relative or absolute. Relative time has a plus (+) or minus (-) sign preceding * the time signature. If the sign is plus (+) then the time value is added to the * returned system time. If the sign is minus (-) then time subtracted from system time. * Absolute time <b>has no</b> preceding symbol in the front of the time signature. * </li> * <li>{@code y} is a year for absolute time or number of years for relative time.</li> * <li> * {@code M} is a month for absolute time or number of months for relative time. * <b>Notice!</b> The month for absolute time is given from 0 to 11 (from January to * December respectively) and follows the general Java convention of managing months. * Also, month can be anything between 0 and 99. Values bigger than eleven (11) increase * years accordingly. * </li> * <li> * {@code d} is a days for absolute time or number of days for relative time. Notice * that {@code d} can be anything between 0 and 999999. This can be useful when * exact relative time shift is needed. For absolute time (dates) only correct days * should be used to avoid strange results. * </li> * <li>{@code H} is hours for absolute and relative time.</li> * <li>{@code m} is minutes for absolute and relative time.</li> * <li>{@code s} is seconds for absolute relative time.</li> * </ul> * * Exact regular expression match pattern for {@code <time>} element is: * <blockquote> * {@code [+-]?\d{1,4}-\d{1,2}-\d{1,6}@\d{1,2}:\d{1,2}:\d{1,2}} * </blockquote> * * * <h3><a name="examples">Example configurations</a></h3> * * This example matches all the classes for the system time shift: * <xmp> * <?xml version="1.0" encoding="UTF-8" ?> * <agent> * <delegate>com.hapiware.asm.timemachine.TimeMachineAgentDelegate</delegate> * <classpath> * <entry>/users/me/agent/target/timemachine-delegate-2.0.0.jar</entry> * <entry>/usr/local/asm-3.1/lib/asm-3.1.jar</entry> * </classpath> * <!-- * Moves time two years and 5 months backward. * The time shift is done for every class. * --> * <configuration>-2-5-0@0:0:0</configuration> * </agent> * </xmp> * * And here is another example: * <xmp> * <?xml version="1.0" encoding="UTF-8" ?> * <agent> * <delegate>com.hapiware.asm.timemachine.TimeMachineAgentDelegate</delegate> * <classpath> * <entry>/users/me/agent/target/timemachine-delegate-2.0.0.jar</entry> * <entry>/usr/local/asm-3.1/lib/asm-3.1.jar</entry> * </classpath> * * * <!-- * Moves time ten days and eight hours forward. * The time shift is done for every loaded class * under com.hapiware.* package. * --> * <filter> * <include>^com/hapiware/.+</include> * </filter> * <configuration>+0-0-10@8:0:0</configuration> * </agent> * </xmp> * * And yet another example: * <xmp> * <?xml version="1.0" encoding="UTF-8" ?> * <agent> * <delegate>com.hapiware.asm.timemachine.TimeMachineAgentDelegate</delegate> * <classpath> * <entry>/users/me/agent/target/timemachine-delegate-2.0.0.jar</entry> * <entry>/usr/local/asm-3.1/lib/asm-3.1.jar</entry> * </classpath> * * <filter> * <include>^com/hapiware/.*f[oi]x/.+</include> * <include>^com/mysoft/.+</include> * <exclude>^com/hapiware/.+/CreateCalculationForm</exclude> * </filter> * <!-- * Set time to 13th of April 2010 7:15 AM. Remember that January is zero (0). * --> * <configuration>2010-3-13@7:15:0</configuration> * </agent> * </xmp> * * * * <h3>Notice! Calculating the relative time value</h3> * For calculating the relative time value there are a few assumptions used: * <ol> * <li>A year has 365 days.</li> * <li>A month has 30 days.</li> * </ol> * * This leads to a little bit inaccurate results, of course, but in practice this doesn't * matter. If the exact shift in time is absolutely needed then days should be calculated * manually and put the manually calculated result to {@code <time>} element without using * years and months at all. * * @see com.hapiware.agent.Agent * * @author <a href="http://www.hapiware.com" target="_blank">hapi</a> */ public class TimeMachineAgentDelegate { /** * Pattern for matching the time configuration string. * <p> * The pattern in general form is: {@code y-M-d@H:m:s} * <p> * Notice that this pattern is a little bit different than what is given in the documentation * (i.e. the grouping is missing). This is because grouping is not needed for examining * a match but necessary to get all the items from the given configuration string. */ private static final String TIME_CONFIGURATION_MATCH_PATTERN = "[+-]?(\\d{1,4})-(\\d{1,2})-(\\d{1,6})@(\\d{1,2}):(\\d{1,2}):(\\d{1,2})"; /** * This method is called by the general agent {@code com.hapiware.agent.Agent} and * is done before the main method call right after the JVM initialisation. * <p> * <b>Notice</b> the difference between this method and * the {@code public static void premain(String, Instrumentation} method described in * {@code java.lang.instrument} package. * * @param includePatterns * A list of patterns to include classes for instrumentation. * * @param excludePatterns * A list patterns to set classes not to be instrumented. * * @param config * {@code String} to set time shift for {@link TimeMachineTransformer}. * * @param instrumentation * See {@link java.lang.instrument.Instrumentation} * * @throws IllegalArgumentException * If there is something wrong with the configuration file. * * @see java.lang.instrument */ public static void premain( Pattern[] includePatterns, Pattern[] excludePatterns, Object config, Instrumentation instrumentation ) { try { if(config != null) { instrumentation.addTransformer( new TimeMachineTransformer( includePatterns, excludePatterns, parseTime((String)config) ) ); } else { String ex = "Time shift configuration is missing."; throw new IllegalArgumentException(ex); } } catch(Exception e) { System.err.println( "Couldn't start the TimeMachine agent delegate due to an exception. " + e.getMessage() ); e.printStackTrace(); } } /** * Parses the configuration string as documented in class description * * @param timeString * A string to be parsed. * * @return * Time ({@link Milliseconds}) parsed from string. */ static Milliseconds parseTime(String timeString) { timeString = timeString.trim(); Pattern p = Pattern.compile(TIME_CONFIGURATION_MATCH_PATTERN); Matcher m = p.matcher(timeString); if(!m.matches()) { String ex = "TimeMachine time configuration string is incorrect."; ex += "\n\t-> This RegEx pattern \"" + TIME_CONFIGURATION_MATCH_PATTERN + "\" was expected" + " but \"" + timeString + "\" was received."; throw new IllegalArgumentException(ex); } int year = Integer.parseInt(m.group(1)); int month = Integer.parseInt(m.group(2)); int date = Integer.parseInt(m.group(3)); int hour = Integer.parseInt(m.group(4)); int minute = Integer.parseInt(m.group(5)); int second = Integer.parseInt(m.group(6)); String firstChar = timeString.substring(0, 1); boolean isRelative = firstChar.equals("+") || firstChar.equals("-"); long millis = 0; if(isRelative) { millis = ((long)year * 365 + (long)month * 30 + (long)date) * 24 * 60 * 60 * 1000; millis += ((long)hour * 60 * 60 + (long)minute * 60 + (long)second) * 1000; if(firstChar.equals("-")) millis = -millis; } else { Calendar c = Calendar.getInstance(); c.clear(); c.set(year, month, date, hour, minute, second); millis = c.getTimeInMillis(); } return new Milliseconds(isRelative, millis); } /** * {@code Milliseconds} is used to hold relative {@literal (i.e. offset)} or absolute time in * milliseconds. If time is a relative value then it is supposed to be added to a returned * system time value. In the case of absolute value then the returned system value is to * be replaced with time hold in {@code Milliseconds}. * <p> * {@code Milliseconds} class is <b>immutable</b>. * * @author hapi * */ static public class Milliseconds { /** * {@code true}, if {@link Milliseconds#time} property has relative time.</br> * {@code false}, if {@link Milliseconds#time} property has absolute time. * * @see Milliseconds#time */ private final boolean isRelative; /** * Time in milliseconds. This value is either relative or absolute value * depending on {@link Milliseconds#isRelative} member. * * @see Milliseconds#isRelative */ private final Long time; /** * Constructs a {@code Milliseconds} object with either absolute or relative time value. * * @param isRelative * {@code true}, if {@code time} is relative time. * {@code false}, if {@code time} is absolute time. * * @param time * Relative or absolute time in milliseconds. */ public Milliseconds(boolean isRelative, long time) { this.isRelative = isRelative; this.time = time; } /** * Indicates if the time value is absolute or relative time. * * @return * {@code true}, if {@code time} is relative time. * {@code false}, if {@code time} is absolute time. * * @see Milliseconds#getTime() */ public boolean isRelative() { return isRelative; } /** * Returns a time value. * * @return * Time in milliseconds. * * @see Milliseconds * @see Milliseconds#isRelative() */ public Long getTime() { return time; } /** * Returns a string representing {@code Milliseconds}. The returned string has one of * the following formats depending on {@link Milliseconds#isRelative} property: * <ol> * <li>{@code {date[DATE], ms[XX]}}</li> * <li>{@code {y[YY], m[MM], d[DD], h[HH], ms[XX]}}</li> * </ol> * where: * <ul> * <li>{@code XX} is time in milliseconds.</li> * <li>{@code DATE} is date as {@link Date#toString()}.</li> * <li>{@code YY} is offset in years.</li> * <li>{@code MM} is offset in months.</li> * <li>{@code DD} is offset in days.</li> * <li>{@code HH} is offset in hours.</li> * </ul> * <p> * The first form is for the absolute value and the second form for the relative value. * <b>Notice</b> that hours, days, months and years all represent the same value but * with different units just to make it a little bit easier for user to examine the value. * * @see Milliseconds#isRelative() */ @Override public String toString() { String retVal = "{"; DecimalFormatSymbols symbols = new DecimalFormatSymbols(); symbols.setDecimalSeparator('.'); DecimalFormat df = new DecimalFormat("0.00", symbols); if(isRelative) { retVal += "y[" + df.format((double)time/(double)(365L * 24L * 60L * 60L * 1000L)) + "], "; retVal += "m[" + df.format((double)time/(double)(30L * 24L * 60L * 60L * 1000L)) + "], "; retVal += "d[" + df.format((double)time/(double)(24L * 60L * 60L * 1000L)) + "], "; retVal += "h[" + df.format((double)time/(double)(60L * 60L * 1000L)) + "], "; } else { Date d = new Date(time); retVal += "date[" + d.toString() + "], "; } retVal += "ms[" + time + "]"; retVal += "}"; return retVal; } } }